<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Image Classification Example with Camera</title>
    <link href="js_example_style.css" rel="stylesheet" type="text/css" />
</head>

<body>
<h2>Image Classification Example with Camera</h2>
<p>
    This tutorial shows you how to write an image classification example with camera.<br>
    To try the example you should click the <b>modelFile</b> button(and <b>configFile</b> button if needed) to upload inference model.
    You can find the model URLs and parameters in the <a href="#appendix">model info</a> section.
    Then You should change the parameters in the first code snippet according to the uploaded model.
    Finally click <b>Start/Stop</b> button to start or stop the camera capture.<br>
</p>

<div class="control"><button id="startAndStop" disabled>Start</button></div>
<div>
    <table cellpadding="0" cellspacing="0" width="0" border="0">
        <tr>
            <td>
                <video id="videoInput" width="400" height="400"></video>
            </td>
            <td>
                <table style="visibility: hidden;" id="result">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col" width=300>Label</th>
                            <th scope="col">Probability</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <th scope="row">1</th>
                            <td id="label0" align="center"></td>
                            <td id="prob0" align="center"></td>
                        </tr>
                        <tr>
                            <th scope="row">2</th>
                            <td id="label1" align="center"></td>
                            <td id="prob1" align="center"></td>
                        </tr>
                        <tr>
                            <th scope="row">3</th>
                            <td id="label2" align="center"></td>
                            <td id="prob2" align="center"></td>
                        </tr>
                    </tbody>
                </table>
                <p id='status' align="left"></p>
            </td>
        </tr>
        <tr>
            <td>
                <div class="caption">
                    videoInput
                </div>
            </td>
            <td></td>
        </tr>
        <tr>
            <td>
                <div class="caption">
                    modelFile <input type="file" id="modelFile">
                </div>
            </td>
        </tr>
        <tr>
            <td>
                <div class="caption">
                    configFile <input type="file" id="configFile">
                </div>
            </td>
        </tr>
    </table>
</div>

<div>
    <p class="err" id="errorMessage"></p>
</div>

<div>
    <h3>Help function</h3>
    <p>1.The parameters for model inference which you can modify to investigate more models.</p>
    <textarea class="code" rows="13" cols="100" id="codeEditor" spellcheck="false"></textarea>
    <p>2.The function to capture video from camera, and the main loop in which will do inference once.</p>
    <textarea class="code" rows="35" cols="100" id="codeEditor1" spellcheck="false"></textarea>
    <p>3.Load labels from txt file and process it into an array.</p>
    <textarea class="code" rows="7" cols="100" id="codeEditor2" spellcheck="false"></textarea>
    <p>4.Get blob from image as input for net, and standardize it with <b>mean</b> and <b>std</b>.</p>
    <textarea class="code" rows="17" cols="100" id="codeEditor3" spellcheck="false"></textarea>
    <p>5.Fetch model file and save to emscripten file system once click the input button.</p>
    <textarea class="code" rows="17" cols="100" id="codeEditor4" spellcheck="false"></textarea>
    <p>6.The post-processing, including softmax if needed and get the top classes from the output vector.</p>
    <textarea class="code" rows="35" cols="100" id="codeEditor5" spellcheck="false"></textarea>
</div>

<div id="appendix">
    <h2>Model Info:</h2>
</div>

<script src="utils.js" type="text/javascript"></script>
<script src="js_dnn_example_helper.js" type="text/javascript"></script>

<script id="codeSnippet" type="text/code-snippet">
inputSize = [224,224];
mean = [104, 117, 123];
std = 1;
swapRB = false;

// record if need softmax function for post-processing
needSoftmax = false;

// url for label file, can from local or Internet
labelsUrl = "https://raw.githubusercontent.com/opencv/opencv/4.x/samples/data/dnn/classification_classes_ILSVRC2012.txt";
</script>

<script id="codeSnippet1" type="text/code-snippet">
let frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
let cap = new cv.VideoCapture(video);

main = async function(frame) {
    const labels = await loadLables(labelsUrl);
    const input = getBlobFromImage(inputSize, mean, std, swapRB, frame);
    let net = cv.readNet(configPath, modelPath);
    net.setInput(input);
    const start = performance.now();
    const result = net.forward();
    const time  = performance.now()-start;
    const probs = softmax(result);
    const classes = getTopClasses(probs, labels);

    updateResult(classes, time);
    setTimeout(processVideo, 0);
    input.delete();
    net.delete();
    result.delete();
}

function processVideo() {
    try {
        if (!streaming) {
            return;
        }
        cap.read(frame);
        main(frame);
    } catch (err) {
        utils.printError(err);
    }
}

setTimeout(processVideo, 0);
</script>

<script id="codeSnippet5" type="text/code-snippet">
softmax = function(result) {
    let arr = result.data32F;
    if (needSoftmax) {
        const maxNum = Math.max(...arr);
        const expSum = arr.map((num) => Math.exp(num - maxNum)).reduce((a, b) => a + b);
        return arr.map((value, index) => {
            return Math.exp(value - maxNum) / expSum;
        });
    } else {
        return arr;
    }
}
</script>

<script type="text/javascript">
    let jsonUrl = "js_image_classification_model_info.json";
    drawInfoTable(jsonUrl, 'appendix');

    let utils = new Utils('errorMessage');
    utils.loadCode('codeSnippet', 'codeEditor');
    utils.loadCode('codeSnippet1', 'codeEditor1');

    let loadLablesCode = 'loadLables = ' + loadLables.toString();
    document.getElementById('codeEditor2').value = loadLablesCode;
    let getBlobFromImageCode = 'getBlobFromImage = ' + getBlobFromImage.toString();
    document.getElementById('codeEditor3').value = getBlobFromImageCode;
    let loadModelCode = 'loadModel = ' + loadModel.toString();
    document.getElementById('codeEditor4').value = loadModelCode;

    utils.loadCode('codeSnippet5', 'codeEditor5');
    let getTopClassesCode = 'getTopClasses = ' + getTopClasses.toString();
    document.getElementById('codeEditor5').value  += '\n' + '\n' + getTopClassesCode;

    let video = document.getElementById('videoInput');
    let streaming = false;
    let startAndStop = document.getElementById('startAndStop');
    startAndStop.addEventListener('click', () => {
        if (!streaming) {
            utils.clearError();
            utils.startCamera('qvga', onVideoStarted, 'videoInput');
        } else {
            utils.stopCamera();
            onVideoStopped();
        }
    });

    let configPath = "";
    let configFile = document.getElementById('configFile');
    configFile.addEventListener('change', async (e) => {
        initStatus();
        configPath = await loadModel(e);
        document.getElementById('status').innerHTML = `The config file '${configPath}' is created successfully.`;
    });

    let modelPath = "";
    let modelFile = document.getElementById('modelFile');
    modelFile.addEventListener('change', async (e) => {
        initStatus();
        modelPath = await loadModel(e);
        document.getElementById('status').innerHTML = `The model file '${modelPath}' is created successfully.`;
        configPath = "";
        configFile.value = "";
    });

    utils.loadOpenCv(() => {
        startAndStop.removeAttribute('disabled');

    });

    var main = async function(frame) {};
    var softmax = function(result){};
    var getTopClasses = function(mat, labels, topK = 3){};

    utils.executeCode('codeEditor1');
    utils.executeCode('codeEditor2');
    utils.executeCode('codeEditor3');
    utils.executeCode('codeEditor4');
    utils.executeCode('codeEditor5');

    function onVideoStarted() {
        streaming = true;
        startAndStop.innerText = 'Stop';
        videoInput.width = videoInput.videoWidth;
        videoInput.height = videoInput.videoHeight;
        utils.executeCode('codeEditor');
        utils.executeCode('codeEditor1');
    }

    function onVideoStopped() {
        streaming = false;
        startAndStop.innerText = 'Start';
        initStatus();
    }

    function updateResult(classes, time) {
        try{
            classes.forEach((c,i) => {
                let labelElement = document.getElementById('label'+i);
                let probElement = document.getElementById('prob'+i);
                labelElement.innerHTML = c.label;
                probElement.innerHTML = c.prob + '%';
            });
            let result = document.getElementById('result');
            result.style.visibility = 'visible';
            document.getElementById('status').innerHTML = `<b>Model:</b> ${modelPath}<br>
                                                         <b>Inference time:</b> ${time.toFixed(2)} ms`;
        } catch(e) {
            console.log(e);
        }
    }

    function initStatus() {
        document.getElementById('status').innerHTML = '';
        document.getElementById('result').style.visibility = 'hidden';
        utils.clearError();
    }

</script>

</body>

</html>